using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;

namespace ErlyThreads
{
    public class ProcessRuntime
    {
        /// <summary>
        /// The list of threads that this master controls.
        /// </summary>
        private IDictionary<object, ThreadContainerBase> mLockedThreads = null;
        private readonly object mThreadsLock = new object();

        private IDictionary<object, IDictionary<Converter<Type, bool>, object>> mLockedSubscriptions = null;
        private readonly object mSubscriptionsLock = new object();

        // doesn't need to be locked, set up in ctor
        private IProcessHandle mOwnerThreadContainerBase = null;

        private readonly bool mconstSendToAllByDefault;

        private IDictionary<object, IList<ReceivedMessage>> mLockMessagesToNonExistingRecipients = null;
        private readonly object mMessagesToNonExistingRecipientsLock = new object();

        public IProcessHandle ProcessHandle
        {
            get { return mOwnerThreadContainerBase; }
        }

        public ProcessRuntime(
            object pOwnerId)
            : this(pOwnerId, false)
        {
        }

        /// <summary>
        /// Constructs a new thread master. It is possible to have multiple 
        /// instances of the ThreadMaster class. Id's are unique to each instance, 
        /// and cross messaging is not supported.
        /// </summary>
        public ProcessRuntime(
            object pOwnerId,
            bool pSendToAllByDefault)
        {
            mconstSendToAllByDefault = pSendToAllByDefault;
            mLockedThreads = new Dictionary<object, ThreadContainerBase>();
            mLockedSubscriptions = new Dictionary<object, IDictionary<Converter<Type, bool>, object>>();
            mLockMessagesToNonExistingRecipients = new Dictionary<object, IList<ReceivedMessage>>();

            InitProcess(
                pOwnerId,
                delegate(IProcessHandle pBase)
                {
                    throw new ApplicationException(
                        "You are not supposed to run the owning thread. It is running in order to own anything");
                });
            mOwnerThreadContainerBase = mLockedThreads[pOwnerId];
        }

        /// <summary>
        /// Inits a new process. Runs on a normal Thread instance.
        /// </summary>
        /// <param name="pId">The Id for the new process</param>
        /// <param name="pStart">The method that this thread will execute with when it runs.</param>
        public void InitProcess(
            object pId,
            Action<IProcessHandle> pStart)
        {
            InitProcess(
                pId,
                pStart,
                false);
        }

        /// <summary>
        /// Initialises the thread. Does not start it.
        /// </summary>
        /// <param name="pId">Will represent the Id for this thread. 
        /// Used later refer to this thread by "name"</param>
        /// <param name="pStart">Represents the method to run when the thread starts up</param>
        /// <param name="pIsShortTaskHint">Provide a hint for if your process is short-lived or not.
        /// If you say true here, you have a chance that your task will be run on a threadpool thread
        /// instead of with a normal new thread instance.</param>
        public void InitProcess(
            object pId, 
            Action<IProcessHandle> pStart,
            bool pHintIsShortTask)
        {
            ThreadContainerBase tbase =
                ThreadContainerBase.ThreadContainerBaseFactory(
                    this,
                    pStart,
                    pId,
                    pHintIsShortTask);

            // the purpose of the strange locking pattern here
            // is to force the messages to be sent to the new thread
            // in the same order as they were received
            lock (mThreadsLock)
            {
                mLockedThreads.Add(
                    pId,
                    tbase);

                lock (mMessagesToNonExistingRecipientsLock)
                {
                    if (mLockMessagesToNonExistingRecipients.ContainsKey(pId))
                    {
                        IList<ReceivedMessage> list = mLockMessagesToNonExistingRecipients[pId];
                        mLockMessagesToNonExistingRecipients.Remove(pId);
                        foreach (ReceivedMessage msg in list)
                        {
                            tbase.Message_Receive(msg);
                        }
                    }
                }
            }

        }

        /// <summary>
        /// Use to send a message to another thread(s)
        /// </summary>
        /// <param name="pId">The "name" of the thread that must receive the message. 
        /// If typeof(pId) is Array, then sends to all threads identified by Ids in the array. 
        /// If null, sends to all. This message will also be sent to all of the subscribers to
        /// this thread's messages.</param>
        /// <param name="pMessage">The "message" that is going to be sent. 
        /// This class must be Serializable, or you will get a runtime exception.</param>
        internal void Message_Send(
            object pIdFrom, 
            object pIdTo, 
            object pMessage)
        {
            IList<object> deliveredToIds = 
                new List<object>();

            if (pIdTo == null)
            {
                if (mconstSendToAllByDefault)
                {
                    Message_Send_To_All(
                        pIdTo,
                        pMessage,
                        deliveredToIds);
                }
            }
            else if (pIdTo is IEnumerable<object>)
            {
                Message_Send_To_Group(
                    pIdFrom,
                    pIdTo as IEnumerable<object>,
                    pMessage,
                    deliveredToIds);
            }
            else
            {
                Message_Send_To_Single(
                    pIdFrom,
                    pIdTo,
                    pMessage,
                    deliveredToIds);
            }

            // we send to the subscribers anyway
            Message_Send_To_Subscribers(
                pIdFrom, 
                pMessage,
                deliveredToIds);
        }

        private void Message_Send_To_Single(
            object pIdFrom,
            object pIdTo,
            object pMessage,
            IList<object> pDeliveredIds)
        {
            // don't deliver the same message to the same recipient twice
            // or don't try to deliver to a thread that has not been inited yet
            if (pDeliveredIds.Contains(pIdTo))
            {
                return;
            }

            ReceivedMessage receivedMessage =
                ReceivedMessage.CreateMessage(
                    pIdFrom,
                    pMessage);

            // we are relying on the principle that a dead-lock
            // will not occur if we acquire locks in the same
            // sequence allways
            // in this case also look at thread start
            // where messages are delivered to newly created
            // threads
            lock (mThreadsLock)
            {
                if (mLockedThreads.ContainsKey(pIdTo))
                {
                    mLockedThreads[pIdTo].Message_Receive(
                        receivedMessage);
                    pDeliveredIds.Add(pIdTo);
                }
                else
                {
                    Message_Send_To_Non_Existing_Thread(
                        pIdTo,
                        receivedMessage,
                        pDeliveredIds);
                }
            }
        }

        private void Message_Send_To_Non_Existing_Thread(
            object pIdTo, 
            ReceivedMessage pReceivedMessage, 
            IList<object> pDeliveredIds)
        {
            lock (mMessagesToNonExistingRecipientsLock)
            {
                IList<ReceivedMessage> list = null;
                if (mLockMessagesToNonExistingRecipients.ContainsKey(pIdTo))
                {
                    list = mLockMessagesToNonExistingRecipients[pIdTo];
                }
                else
                {
                    mLockMessagesToNonExistingRecipients.Add(
                        pIdTo,
                        list = new List<ReceivedMessage>());
                }

                list.Add(pReceivedMessage);
            }

            pDeliveredIds.Add(pIdTo);
        }

        private void Message_Send_To_Group(
            object pIdFrom,
            IEnumerable<object> pIdTos,
            object pMessage,
            IList<object> pDeliveredIds)
        {
            foreach (object id in pIdTos)
            {
                Message_Send_To_Single(
                    pIdFrom,
                    id,
                    pMessage,
                    pDeliveredIds);
            }
        }

        private void Message_Send_To_All(
            object pIdFrom,
            object pMessage,
            IList<object> pDeliveredIds)
        {
            ICollection<object> threadIds = null;
            lock (mThreadsLock)
            {
                threadIds = mLockedThreads.Keys;
            }

            Message_Send_To_Group(
                pIdFrom,
                threadIds,
                pMessage,
                pDeliveredIds);
        }

        private void Message_Send_To_Subscribers(
            object pIdFrom,
            object pMessage,
            IList<object> pDeliveredIds)
        {
            IDictionary<Converter<Type, bool>, object> subscriptions = null;

            // Make a copy of the subscriptions, and send to those
            lock(mLockedSubscriptions)
            {
                if (mLockedSubscriptions.Keys.Contains(pIdFrom))
                {
                    subscriptions = new Dictionary<Converter<Type, bool>, object>();
                    foreach (KeyValuePair<Converter<Type, bool>, object> kvp in
                        mLockedSubscriptions[pIdFrom])
                    {
                        subscriptions.Add(new KeyValuePair<Converter<Type, bool>, object>(
                            kvp.Key,
                            kvp.Value));
                    }
                }
            }

            // don't send if nobody subscribed
            if (subscriptions == null)
            {
                return;
            }

            Type messageType = pMessage != null ?
                        pMessage.GetType() :
                        null;
            foreach (KeyValuePair<Converter<Type, bool>, object> kvp in subscriptions)
            {
                if (kvp.Key(messageType))
                {
                    Message_Send_To_Single(
                        pIdFrom,
                        kvp.Value,
                        pMessage,
                        pDeliveredIds);
                }
            }
        }

        /// <summary>
        /// Receive all message sent by this thread
        /// </summary>
        /// <param name="pSenderId">The "name" of the thread that will be sending the messages</param>
        /// <param name="pReceiverId">The "name" of the thread that will be receiving the messages</param>
        /// <param name="pFilter">Acts as a filter. You will be passed the messages' type 
        /// that is sent by the thread. Return true, if you want to receive this type of message.</param>
        internal void Messages_Subscribe(
            object pSenderId, 
            object pReceiverId, 
            Converter<Type, bool> pFilter)
        {
            lock (mSubscriptionsLock)
            {
                IDictionary<Converter<Type, bool>, object> subscriptions = null;

                // make sure that the ThreadMaster knows that the sending thread has subscriptions
                if (!mLockedSubscriptions.Keys.Contains(pSenderId))
                {
                    subscriptions = new Dictionary<Converter<Type, bool>, object>();
                    mLockedSubscriptions.Add(pSenderId, subscriptions);
                }
                else
                {
                    subscriptions = mLockedSubscriptions[pSenderId];
                }

                // add the new subscription
                subscriptions.Add(pFilter, pReceiverId);
            }
        }

        /// <summary>
        /// Starts the thread identified by pId. Throws if that thread is running already.
        /// </summary>
        /// <param name="pId">the Id for the thread that must start.</param>
        public void StartProcess(object pId)
        {
            lock (mThreadsLock)
            {
                mLockedThreads[pId].Start();
            }
        }

    }
}
